﻿// Copyright 2014 The Noda Time Authors. All rights reserved.
// Use of this source code is governed by the Apache License 2.0,
// as found in the LICENSE.txt file.

using NodaTime.Annotations;
using NodaTime.Utility;
using System;
using System.Diagnostics.CodeAnalysis;

namespace NodaTime.Calendars
{
    /// <summary>
    /// Implementation of the Um Al Qura calendar, using the tabular data in the BCL. This is fetched
    /// on construction and cached - we just need to know the length of each month of each year, which is
    /// cheap as the current implementation only covers 183 years.
    /// </summary>
    internal sealed class UmAlQuraYearMonthDayCalculator : RegularYearMonthDayCalculator
    {
        private const int AverageDaysPer10Years = 3544;

        // These four members are generated by UmAlQuraYearMonthDayCalculatorTest.GenerateData.
        private const int ComputedMinYear = 1318;
        private const int ComputedMaxYear = 1500;
        private const int ComputedDaysAtStartOfMinYear = -25448;
        private const string GeneratedData =
            "AAAF1A3SHaQdSBqUFSwKbBVqG1QXSBaSFSYKVhSuCWwVagtUGqoaVBSsCVwSugXYDaoNVAqqCVYStgV0" +
            "CuoXZA7IDpIMqgVWCrYVtA2oHZIbJBpKFJoFWgraFtQWpBVKFJYJLhJuBWwK6hrUGqQVLBJaBLoJuhW0" +
            "C6gbUhqkFVQJrBNsBugO0g6kDUoKlhVWCrQVqhukG0gakhUqCloUugq0FaoNVA0qClYUrglcEuwK2Baq" +
            "FVQUqglaEroFtAuyG2QXSBaUFKoFagrqFtQXpBeIFxIVKgpaC1oW1A2oG5IbJBVMEqwFXAraBtQWqhVU" +
            "EpoJOhK6BXQLagtUGqoVNBJcBNwKuhW0DagNSgqWFS4KnBVcC1gXUhskFkoMlhlWCrQWqg2kHUoclBUq" +
            "CloVWgbYDrINpA0qCloUtgl0E3QHaBbSFqQVTAlsEtoF2A2yHWQaqBpUFKwJXBLaGtQWqBZSFSYKVhSu" +
            "CmwVag1UHSYAAA==";

        private const int ComputedDaysAtStartOfYear1 = ComputedDaysAtStartOfMinYear + (int) (((1 - ComputedMinYear) / 10.0) * AverageDaysPer10Years);
        // Precomputed values for lengths of year and lengths of months, populated in the static constructor.
        // The number of days in each year, with array index 1 representing ComputedMinYear.
        private static readonly int[] YearLengths;
        // A set of 12 bits for each year, indicating the months which are 30 days long instead of 29.
        // (Month 1 is set in bit 1, etc. Bit 0 and bits 13-15 are unused.)
        private static readonly ushort[] MonthLengths;
        // Number of days from ComputedMinYear on a per year basis.
        private static readonly int[] YearStartDays;

        static UmAlQuraYearMonthDayCalculator()
        {
            byte[] data = Convert.FromBase64String(GeneratedData);
            MonthLengths = new ushort[data.Length / 2];
            for (int i = 0; i < MonthLengths.Length; i++)
            {
                MonthLengths[i] = (ushort) ((data[i * 2] << 8) | (data[i * 2 + 1]));
            }
            YearLengths = new int[MonthLengths.Length];
            YearStartDays = new int[MonthLengths.Length];

            // Populate arrays from index 1.
            int totalDays = 0;
            for (int year = 1; year < YearLengths.Length - 1; year++)
            {
                YearStartDays[year] = ComputedDaysAtStartOfMinYear + totalDays;
                int monthBits = MonthLengths[year];
                int yearLength = 29 * 12;
                for (int month = 1; month <= 12; month++)
                {
                    yearLength += (monthBits >> month) & 1;
                }
                YearLengths[year] = yearLength;
                totalDays += yearLength;
            }
            // Fill in the cache with dummy data for before/after the min/max year, pretending
            // that both of the "extra" years were 354 days long.
            YearStartDays[0] = ComputedDaysAtStartOfMinYear - 354;
            YearStartDays[YearStartDays.Length - 1] = ComputedDaysAtStartOfMinYear + totalDays;
            YearLengths[0] = 354;
            YearLengths[YearStartDays.Length - 1] = 354;
        }

        internal UmAlQuraYearMonthDayCalculator()
            : base(ComputedMinYear, ComputedMaxYear, 12, AverageDaysPer10Years, ComputedDaysAtStartOfYear1)
        {
        }

        // No need to use the YearMonthDayCalculator cache, given that we've got the value in array already.
        internal override int GetStartOfYearInDays([Trusted] int year) => YearStartDays[year - ComputedMinYear + 1];

        [ExcludeFromCodeCoverage]
        protected override int CalculateStartOfYearDays([Trusted] int year)
        {
            // Only called from the base GetStartOfYearInDays implementation.
            throw new NotImplementedException();
        }

        protected override int GetDaysFromStartOfYearToStartOfMonth([Trusted] int year, [Trusted] int month)
        {
            // While we could do something clever to find the Hamming distance on the masked value here,
            // it's considerably simpler just to iterate...
            int monthBits = MonthLengths[year - ComputedMinYear + 1];
            int extraDays = 0;
            for (int i = 1; i < month; i++)
            {
                extraDays += (monthBits >> i) & 1;
            }
            return (month - 1) * 29 + extraDays;
        }

        internal override int GetDaysInMonth([Trusted] int year, int month)
        {
            int monthBits = MonthLengths[year - ComputedMinYear + 1];
            return 29 + ((monthBits >> month) & 1);
        }

        internal override int GetDaysInYear([Trusted] int year) =>
            // Fine for one year either side of min/max.
            YearLengths[year - ComputedMinYear + 1];

        internal override YearMonthDay GetYearMonthDay([Trusted] int year, [Trusted] int dayOfYear)
        {
            int daysLeft = dayOfYear;
            int monthBits = MonthLengths[year - ComputedMinYear + 1];
            for (int month = 1; month <= 12; month++)
            {
                int monthLength = 29 + ((monthBits >> month) & 1);
                if (daysLeft <= monthLength)
                {
                    return new YearMonthDay(year, month, daysLeft);
                }
                daysLeft -= monthLength;
            }
            // This should throw...
            Preconditions.CheckArgumentRange(nameof(dayOfYear), dayOfYear, 1, GetDaysInYear(year));
            throw new InvalidOperationException($"Bug in Noda Time: year {year} has {GetDaysInYear(year)} days but {dayOfYear} isn't valid");
        }

        internal override bool IsLeapYear([Trusted] int year) => YearLengths[year - ComputedMinYear + 1] == 355;
    }
}
